package cn.webwheel.template;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.mvel2.MVEL;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.lang.reflect.Array;
import java.net.URLEncoder;
import java.util.*;

public class TemplateRenderer {

    enum OutputContext {PlainText, AttributeValue, Javascript, JavascriptString, HrefValue}

    ObjectMapper objectMapper;

    List<Object> content;

    public String charset;
    public String contentType;

    public TemplateRenderer(List<Object> content, ObjectMapper objectMapper) {
        this.content = content;
        this.objectMapper = objectMapper;
    }

    public void render(Writer writer, Map<String, Object> ctx) throws IOException {
        write(writer, ctx, content);
    }

    private void write(Writer writer, Map<String, Object> ctx, List<Object> content) throws IOException {
        for (Object obj : content) {
            if (obj instanceof Var) {
                writer.write(output((Var) obj, ctx));
            } else if (obj instanceof Attr) {
                Attr attr = (Attr) obj;
                Object exp = attr.var.exp;
                Object v = MVEL.executeExpression(exp, ctx);
                if (v != null && v != Boolean.FALSE) {
                    writer.write(' ');
                    writer.write(attr.name);
                    writer.write("=\"");
                    if (v != Boolean.TRUE) {
                        writer.write(output(attr.var, ctx));
                    }
                    writer.write("\"");
                }
            } else if (obj instanceof Block) {
                Block block = (Block) obj;
                write(writer, ctx, block.vars, block.content);
            } else {
                writer.write(obj.toString());
            }
        }
    }

    private void write(Writer writer, Map<String, Object> ctx, LinkedHashMap<String, Var> vars, List<Object> content) throws IOException {
        Iterator<Map.Entry<String,Var>> it = vars.entrySet().iterator();
        Map.Entry<String, Var> entry = it.next();
        Object obj = MVEL.executeExpression(entry.getValue().exp, ctx);
        Iterator iter = null;
        if (entry.getKey().isEmpty()) {
            if (obj == null || obj == Boolean.FALSE) return;
            if (obj != Boolean.TRUE) {
                writer.write(obj.toString());
                return;
            }
        } else {
            if (obj == null) return;
            if (!entry.getValue().raw) {
                if (obj instanceof Iterator) {
                    iter = (Iterator) obj;
                } else if (obj instanceof Iterable) {
                    iter = ((Iterable) obj).iterator();
                } else if (obj.getClass().isArray()) {
                    iter = new ArrayIterator(obj);
                }
            }
        }
        LinkedHashMap<String, Var> nvars = null;
        if (it.hasNext()) {
            nvars = new LinkedHashMap<String, Var>();
            do {
                entry = it.next();
                nvars.put(entry.getKey(), entry.getValue());
            } while (it.hasNext());
        }
        HashMap<String, Object> nctx = new HashMap<String, Object>(ctx);
        if (iter == null) {
            nctx.put(entry.getKey(), obj);
            if (nvars != null) {
                write(writer, nctx, nvars, content);
            } else {
                write(writer, nctx, content);
            }
        } else {
            int idx = 0;
            while (iter.hasNext()) {
                nctx.put(entry.getKey() + "_idx", idx++);
                nctx.put(entry.getKey(), iter.next());
                if (nvars != null) {
                    write(writer, nctx, nvars, content);
                } else {
                    write(writer, nctx, content);
                }
            }
        }
    }

    private String output(Var var, Map<String, Object> ctx) throws IOException {
        Object exp = var.exp;
        Object obj = MVEL.executeExpression(exp, ctx);
        if (obj == null) return "";
        if (var.raw) return obj.toString();
        switch (var.ctx) {
            case PlainText:
                return obj.toString().replace("<", "&lt;");
            case AttributeValue:
                return obj.toString().replace("\"", "&quot;");
            case Javascript:
                return objectMapper.writeValueAsString(obj);
            case JavascriptString:
                String s = objectMapper.writeValueAsString(obj.toString());
                return s.substring(1, s.length() - 1);
            case HrefValue:
                try {
                    return URLEncoder.encode(obj.toString(), "utf-8");
                } catch (UnsupportedEncodingException e) {
                    return obj.toString();
                }
        }
        return obj.toString();
    }

    private static class ArrayIterator implements Iterator {

        Object array;
        int i;
        int c;

        private ArrayIterator(Object array) {
            this.array = array;
            c = Array.getLength(array);
        }

        @Override
        public boolean hasNext() {
            return i < c;
        }

        @Override
        public Object next() {
            return Array.get(array, i++);
        }

        @Override
        public void remove() {
        }
    }

    static class Var {
        OutputContext ctx;
        Object exp;
        boolean raw;
    }

    static class Attr {
        String name;
        Var var;
    }

    static class Block {
        LinkedHashMap<String, Var> vars;
        List<Object> content;
    }
}
